<?php
/**
 * @author css3 <css3@qq.com>
 */

namespace zonday\weixin;

use Yii;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\helpers\Json;
use yii\web\HttpException;

use zonday\weixin\crypt\WXBizMsgCrypt;
use zonday\weixin\event\Click;
use zonday\weixin\event\MassSendJobFinish;
use zonday\weixin\event\TemplateSendJobFinish;
use zonday\weixin\event\Scan;
use zonday\weixin\event\Subscribe;
use zonday\weixin\event\Unsubscribe;
use zonday\weixin\event\Location as EventLocation;
use zonday\weixin\event\View;
use zonday\weixin\exception\WeixinException;
use zonday\weixin\exception\WeixinErrorException;
use zonday\weixin\message\Image;
use zonday\weixin\message\Link;
use zonday\weixin\message\Location as MessageLocation;
use zonday\weixin\message\Message;
use zonday\weixin\message\ShortVideo;
use zonday\weixin\message\Text;
use zonday\weixin\message\Video;
use zonday\weixin\message\Voice;

/**
 * Class Weixin
 */
class Weixin extends Component
{
    /**
     * 基础接口 access_token 获取地址
     */
    const TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/token';

    /**
     * 网页授权地址
     */
    const WEB_AUTHORIZE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize';

    /**
     * 微信网页授权 access_token 获取地址
     */
    const WEB_ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token';

    /**
     * 微信网页搜权 snsapi_userinfo 获取地址
     */
    const SNS_USER_INFO_URL = 'https://api.weixin.qq.com/sns/userinfo';

    /**
     * 微信网页授权 access_token 刷新地址
     */
    const WEB_REFRESH_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/refresh_token';

    /**
     * api 基础地址
     */
    const API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin';

    /**
     * @var string 公众号appID
     */
    public $appId;

    /**
     * @var string 公众号appsecret
     */
    public $appSecret;

    /**
     * @var string 接口配置中的token
     */
    public $token;

    /**
     * @var string 消息加密key
     */
    public $encodingAesKey;

    /**
     * @var string
     */
    public $tokenStoreClass = 'zonday\weixin\store\FileStore';

    /**
     * @var \zonday\weixin\store\Store
     */
    protected $tokenStore;

    /**
     * @var string 加密类型
     */
    protected $encryptType = 'raw';

    /**
     * @throws InvalidConfigException
     */
    public function init()
    {
        parent::init();

        if (!isset($this->appId, $this->appSecret, $this->token)) {
            throw new InvalidConfigException('appId, appSecret, token 必须设置');
        }

        $this->tokenStore = Yii::createObject(['class' => $this->tokenStoreClass]);
    }

    /**
     * @param string $openid
     * @param string $accessToken
     * @return mixed
     */
    public function snsUserInfo($openid, $accessToken)
    {
        return $this->request(self::SNS_USER_INFO_URL, [
            'openid' => $openid,
            'access_token' => $accessToken,
        ]);
    }

    /**
     * @param $apiSubUrl
     * @param null $get
     * @param null $post
     * @param array $options
     * @return mixed
     * @throws Exception
     * @throws HttpException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function api($apiSubUrl, $get = null, $post = null, $options = [])
    {
        if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) {
            $url = $apiSubUrl;
        } else {
            $url = self::API_BASE_URL . '/' . $apiSubUrl;
        }
        $get['access_token'] = $this->getAccessToken();
        $options = array_merge([
            'raw' => false,
            'jsonEncode' => true,
        ], $options);
        if (is_array($post) && $options['jsonEncode']) {
            $post = json_encode($post, JSON_UNESCAPED_UNICODE);
        }
        return $this->request($url, $get, $post, $options['raw']);
    }

    /**
     * 获取基础接口中的访问token
     *
     * @param bool $refresh 是否刷新
     * @return string
     * @throws Exception
     * @throws HttpException
     */
    public function getAccessToken($refresh = false)
    {
        $accessToken = $this->tokenStore->read('accessToken');
        if ($refresh || !is_array($accessToken) || $accessToken['expires'] < time()) {
            $result = $this->request(
                self::TOKEN_URL,
                array(
                    'grant_type' => 'client_credential',
                    'appid' => $this->appId,
                    'secret' => $this->appSecret,
                )
            );

            $accessToken = array(
                'value' => $result['access_token'],
                'expires' => time() + $result['expires_in'],
            );
            $this->tokenStore->write('accessToken', $accessToken);
        }

        return $accessToken['value'];
    }

    /**
     * 获取网页授权的access token
     * @param $code
     * @return mixed
     * @throws Exception
     * @throws InvalidResponseException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function fetchAuthorizeAccessToken($code)
    {
        $result = $this->request(
            self::WEB_ACCESS_TOKEN_URL,
            array(
                'appid' => $this->appId,
                'secret' => $this->appSecret,
                'code' => $code,
                'grant_type' => 'authorization_code',
            )
        );
        $result['expires'] = time() + $result['expires_in'];
        $result['refresh_expires'] = time() + 7 * 86400;
        return $result;
    }

    /**
     * 刷新授权的access token
     * @param $refreshToken
     * @return mixed
     * @throws Exception
     * @throws InvalidResponseException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function refreshAuthorizeAccessToken($refreshToken)
    {
        $result = $this->request(
            self::WEB_REFRESH_TOKEN_URL,
            array(
                'appid' => $this->appId,
                'refresh_token' => $refreshToken,
                'grant_type' => 'refresh_token',
            )
        );
        $result['expires'] = time() + $result['expires_in'];
        $result['refresh_expires'] = time() + 7 * 86400;
        return $result;
    }

    /**
     * 获取网页授权url
     *
     * @param string $redirectUri 跳转地址
     * @param string $scope
     * @param string $state
     * @return string
     */
    public function webAuthorizeUrl($redirectUri, $scope = 'snsapi_userinfo', $state = '')
    {
        $params = array(
            'appid' => $this->appId,
            'redirect_uri' => $redirectUri,
            'response_type' => 'code',
            'scope' => $scope,
        );

        if ($state) {
            $params['state'] = $state;
        }
        return self::WEB_AUTHORIZE_URL . '?' . http_build_query($params, null, '&') . '#wechat_redirect';
    }

    /**
     * @param $post
     * @param array $params
     * @return array|bool|string
     * @throws WeixinException
     */
    public function receive($post, $params = [])
    {
        if (!$this->checkSignature($params)) {
            $request = Yii::$app->getRequest();
            throw new WeixinException(sprintf('微信信息验证失败，url:%s', $request->getUrl()));
        }

        $data = $post;

        if (isset($params['encrypt_type']) && isset($params['msg_signature'])) {
            $this->encryptType = $params['encrypt_type'];
            if ($params['encrypt_type'] == 'aes' && $post) {
                $pc = new WXBizMsgCrypt($this->token, $this->encodingAesKey, $this->appId);
                $code = $pc->decryptMsg($params['msg_signature'], $params['timestamp'], $params['nonce'], $post, $data);
                if ($code !== 0) {
                    throw new WeixinException(sprintf('微信消息解密失败，code:%s', $code));
                }
            }
        }

        if ($data) {
            $result = simplexml_load_string($data);
            if ($result === false) {
                throw new WeixinException(sprintf('微信解析xml出错: %s', $data));
            } else {
                $message = array();
                foreach ((array) $result as $name => $value) {
                    $message[(string) $name] = (string) $value;
                }
                return $this->processMessage($message);
            }
        } else {
            return isset($params['echostr']) ? $params['echostr'] : '';
        }
    }

    /**
     * 回复微信信息
     *
     * @param Message $message
     * @return string
     * @throws Exception
     */
    public function reply(Message $message) {
        $str = (string) $message;
        if ($this->encryptType !== 'raw') {
            $pc = new WXBizMsgCrypt($this->token, $this->encodingAesKey, $this->appId);
            $code = $pc->encryptMsg((string) $message, time(), Yii::$app->getSecurity()->generateRandomString(6), $str);
            if ($code !== 0) {
                throw new WeixinException(sprintf('微信消息加密失败，code:%s', $code));
            }
        }
        return $str;
    }

    /**
     * @param $message
     * @return Message
     * @throws WeixinException
     */
    public function processMessage($message)
    {
        $msgType = isset($message['MsgType']) ? $message['MsgType'] : null;
        unset($message['MsgType']);
        switch ($msgType) {
            case 'text':
                return new Text($message);
            case 'image':
                return new Image($message);
            case 'voice':
                return new Voice($message);
            case 'video':
                return new Video($message);
            case 'shortvideo':
                return new ShortVideo($message);
            case 'location':
                return new MessageLocation($message);
            case 'link':
                return new Link($message);
            case 'event':
                $event = isset($message['Event']) ? $message['Event'] : null;
                unset($message['Event']);
                switch (strtolower($event)) {
                    case 'subscribe':
                        return new Subscribe($message);
                    case 'unsubscribe':
                        return new Unsubscribe($message);
                    case 'scan':
                        return new Scan($message);
                    case 'location':
                        return new EventLocation($message);
                    case 'click':
                        return new Click($message);
                    case 'view':
                        return new View($message);
                    case 'masssendjobfinish':
                        return new MassSendJobFinish($message);
                    case 'templatesendjobfinish':
                        return new TemplateSendJobFinish($message);
                    default:
                        throw new WeixinException('不支持微信事件类型');
                }
            default:
                throw new WeixinException('不支持微信消息类型');
        }
    }


    /**
     * 请求数据
     *
     * @param $url
     * @param null $get
     * @param null $post
     * @param bool $raw
     * @return mixed
     * @throws Exception
     * @throws InvalidResponseException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function request($url, $get = null, $post = null, $raw = false)
    {
        if (is_array($get)) {
            $url .= '?' . http_build_query($get, '', '&');
        }

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

        if ($post !== null) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
        }

        Yii::trace($url . "\n" . var_export($post, true), __METHOD__);

        $response = curl_exec($ch);
        $responseHeaders = curl_getinfo($ch);

        $errorNumber = curl_errno($ch);
        $errorMessage = curl_error($ch);

        curl_close($ch);

        if ($errorNumber > 0) {
            throw new Exception('Curl error requesting "' .  $url . '": #' . $errorNumber . ' - ' . $errorMessage);
        }

        if (strncmp($responseHeaders['http_code'], '20', 2) !== 0) {
            throw new InvalidResponseException($responseHeaders, $response, 'Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response);
        }

        if ($raw) {
            return $response;
        }

        $result = json_decode($response, true);

        if (!is_array($result)) {
            throw new WeixinException(sprintf('微信json解析异常。 Url: %s', $url));
        } elseif (isset($result['errcode']) && $result['errcode'] != 0) {
            throw new WeixinErrorException('#' . $result['errcode'] . ' '. (isset($result['errmsg']) ? $result['errmsg'] : $result['errcode']), $result['errcode']);
        } else {
            return $result;
        }
    }


    /**
     * 验证微信消息签名
     *
     * @param array $query
     * @return bool
     */
    public function checkSignature($query = [])
    {
        if (empty($query["signature"]) ||
            empty($query["timestamp"]) ||
            empty($query["nonce"])) {
            return false;
        }

        $signature = $query["signature"];
        $array = array($this->token, $query["timestamp"], $query["nonce"]);
        sort($array, SORT_STRING);
        return sha1(implode(($array))) === $signature;
    }

    /**
     * 获取 Js Api ticket
     * @param bool $refresh
     * @return mixed
     * @throws Exception
     * @throws InvalidResponseException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function getJsApiTicket($refresh = false)
    {
        $jsApiTicket = $this->tokenStore->read('jsApiTicket');
        if ($refresh || !is_array($jsApiTicket) || $jsApiTicket['expires'] < time()) {
            $result = $this->api('ticket/getticket', ['type' => 'jsapi']);
            $jsApiTicket = array(
                'value' => $result['ticket'],
                'expires' => time() + $result['expires_in'],
            );
            $this->tokenStore->write('jsApiTicket', $jsApiTicket);
        }

        return $jsApiTicket['value'];
    }

    /**
     * 获取weixin Js Api 配置
     * @param string $url
     * @param array $apiList
     * @param array $options
     * @return array
     */
    public function jsApiConfig($url, $apiList = [], $options = [])
    {
        $options = array_merge([
            'debug' => false,
            'timestamp' => time(),
            'nonceStr' => Yii::$app->getSecurity()->generateRandomString(6),
            'appId' => $this->appId,
        ], $options);

        if (isset($options['ticket'])) {
            $ticket = $options['ticket'];
            unset($options['ticket']);
        } else {
            $ticket = $this->getJsApiTicket();
        }

        $options['jsApiList'] = $apiList;
        $options['signature'] = sha1("jsapi_ticket={$ticket}&noncestr={$options['nonceStr']}&timestamp={$options['timestamp']}&url={$url}");
        return $options;
    }

    /**
     * 获取微信卡片ticket
     * @param bool $refresh
     * @return mixed
     * @throws Exception
     * @throws InvalidResponseException
     * @throws WeixinErrorException
     * @throws WeixinException
     */
    public function getWxCardTicket($refresh = false)
    {
        $wxCardTicket = $this->tokenStore->read('wxCardTicket');
        if ($refresh || !is_array($wxCardTicket) || $wxCardTicket['expires'] < time()) {
            $result = $this->api('ticket/getticket', ['type' => 'wx_card']);
            $wxCardTicket = array(
                'value' => $result['ticket'],
                'expires' => time() + $result['expires_in'],
            );
            $this->tokenStore->write('wxCardTicket', $wxCardTicket);
        }

        return $wxCardTicket['value'];
    }

    /**
     * 微信卡片扩展数据
     * @param string $code
     * @param string $openid
     * @param array $options
     * @return array
     */
    public function cardExt($code = '', $openid = '', $options = [])
    {
        $options = array_merge([
            'timestamp' => time(),
            'nonceStr' => Yii::$app->getSecurity()->generateRandomString(6),
        ], $options);
        $options['code'] = $code;
        $options['openid'] = $openid;

        if (isset($options['ticket'])) {
            $ticket = $options['ticket'];
            unset($options['ticket']);
        } else {
            $ticket = $this->getWxCardTicket();
        }

        $signatureArray = [$options['timestamp'], $options['nonceStr'], $options['code'], $options['openid'], $ticket];
        sort($signatureArray, SORT_STRING);
        $options['signature'] = sha1(implode(($signatureArray)));
        return $options;
    }

    /**
     * 微信卡片签名
     * @param string $cardId
     * @param string $cardType
     * @param string $locationId
     * @param array $options
     * @return string
     */
    public function cardSign($cardId, $cardType, $locationId, $options = [])
    {
        $options = array_merge([
            'timestamp' => time(),
            'nonceStr' => Yii::$app->getSecurity()->generateRandomString(6),
            'appId' => $this->appId,
        ], $options);
        $signatureArray = [$options['timestamp'], $options['nonceStr'], $cardId, $cardType, $locationId, $this->getWxCardTicket()];
        sort($signatureArray, SORT_STRING);
        return sha1(implode(($signatureArray)));
    }
}